package com.googlecode.cswish.util;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;
import org.mvel2.MVEL;
import org.springframework.beans.factory.annotation.Autowired;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.model.KeyValue;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PageElementsTextModel;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.DynamicAction;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.conversion.PageConverter;
import com.googlecode.cswish.struts.hibernate.HibernateUtil;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ModelPropertyInfo;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.struts.spring.SystemEnv;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;

/**
 * Provide the common functions (Sigleton)
 * 
 * @author Jerry.Feng Date: 2008-5-30
 */
public class Functions {

	private static final Logger logger = Logger.getLogger(Functions.class);

	private static String base;
	
	private long id;
	
	protected String actionExtension = "html";
	protected String actionExtensionSuffix = ".html";
	
	@Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
	public void setActionExtension(String actionExtension) {
		this.actionExtension = actionExtension;
		this.actionExtensionSuffix = '.' + actionExtension;
	}
	
	private boolean debug;
	
	public boolean isDebug() {
		return this.debug;
	}
	
	@Resource
	protected ObjectFactory objectFactory;
	
	@Resource
	protected BeanIntrospector beanIntrospector;
	
	@Resource
	protected OgnlExUtil ognlExUtil;
	
	
	@Resource
	protected Config config;
	
	@Resource
	protected SafeManager safeManager;
	
	@Resource
	protected Routing routing;
	
	protected TagUtil tagUtil;
	
	private Map<String, FunctionCallback> exFuncs;
	private Map<String, Object> commonServiceMap;
	
	@PostConstruct
	public void init() {
		exFuncs = new ConcurrentHashMap<String, FunctionCallback>();
		commonServiceMap = new ConcurrentHashMap<String, Object>();
		
		// Output the simple date
		exFuncs.put("sd", new FunctionCallback() {
			@Override
			public Object execute(Object value,Object bean, String property,  Object[] args) {
				if (value instanceof Date) {
					value = new SimpleDateFormat("yyyy-MM-dd").format(value);
				}
				return value;
			}
		});
		// call the matched service
		exFuncs.put("sv", new FunctionCallback() {
			@Override
			public Object execute(Object value,Object bean, String property,  Object[] args) {
				String className;
				String subProperty;
				int index = property.indexOf('.');
				if (index > 0) {
					String shortName = property.substring(0, index);
					
					className = beanIntrospector.guessFullClassName(shortName.toLowerCase());
					subProperty = property.substring(index + 1);
				} else {
					if (bean instanceof DynamicAction) {
						className = ((DynamicAction)bean).getActionName();
					} else {
						className = ClassUtil.getClassName(bean);
					}
					subProperty = property;
				}
				if (className == null) {
					// maybe, the PageElementInterceptor activates this method, so the action name is null
					return null;
				}
				
				
				String simpleName = beanIntrospector.getSimpleName(className);
				int methodEnd = subProperty.indexOf('.');
				String exProperty;
				String method;
				if (methodEnd == -1) {
					method = subProperty;
					exProperty = null;
				} else {
					method = subProperty.substring(0, methodEnd);
					exProperty = subProperty.substring(methodEnd + 1);
				}
				
				List params = args == null ? null : Arrays.asList(args);
				Object ret = service(simpleName, method, params);
				
				if (ret != null && exProperty != null) {
					ret = MVEL.getProperty(exProperty, ret);
				}
				return ret;
			}
		});
		
		debug = "true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE));
	}
	
	public void registExFunction(String name, FunctionCallback fc) {
		exFuncs.put(name, fc);
	}
	
	public FunctionCallback getExFunction(String funcName) {
		return exFuncs.get(funcName);
	}
	
	public void registCommonService(String name, Object service) {
		Object existed = commonServiceMap.put(name, service);
		if (existed != null) {
			if (logger.isEnabledFor(Level.WARN)) {
				String invokerClassName = Thread.currentThread().getStackTrace()[2].getClassName();
				logger.warn(String.format("%s register duplicated service for %s, the old service is %s, the new service is %s"
						, invokerClassName, name, existed.getClass().getName(), service.getClass().getName()));
			}
		}
	}
	
	@Autowired
	protected void setConfig(Config config) {
		this.config = config;
	}
	
	private GenericService getGenericService() {
		return (GenericService) service("generic");
	}
	
	private InvocationUtil invocationUtil;
	
	protected InvocationUtil getInvocationUtil() {
		if (invocationUtil == null) {
			invocationUtil = objectFactory.getBean(InvocationUtil.class, false);
		}
		return invocationUtil;
	}
	
	private PageConverter pageConverter;
	
	protected PageConverter getPageConverter() {
		if (pageConverter == null) {
			pageConverter = objectFactory.getBean(PageConverter.class, false);
		}
		return pageConverter;
	}
	
	public boolean permit(String url, String method) {
		HttpServletRequest request = ServletActionContext.getRequest();
		UserInfo userInfo = safeManager.getLoginUser(ServletActionContext.getRequest());
		if (userInfo == null) {
			return false;
		}
		
		String userId = userInfo.getLoginId();
		int end = url.indexOf('?');
		if (end == -1) {
			end = url.length();
		}
		
		if (url.length() == 0) {
			url = Routing.getUri(request);
		} else if (url.startsWith(getBase())) {
			url = url.substring(getBase().length(), end);
		} else {
			url = url.substring(0, end);
		}
		
		String path;
		if (method != null) {
			int methodIndex = url.lastIndexOf('.', url.length() - actionExtension.length() - 2);
			path = url.substring(0, methodIndex + 1) + method + actionExtensionSuffix;
		} else {
			path = url;
		}
		return safeManager.checkPage(userId, path);
	}
	
	public static void setBase(String base) {
		Functions.base = base;
	}
	
	public static String getBase() {
		if (base != null) {
			return base;
		}
		
		base = ServletActionContext.getServletContext().getContextPath();
		return base;
	}
	
	public String getActionExtension() {
		return this.actionExtension;
	}
	
	public String getSiteName() {
		String site = ContextHolder.get().getSiteName();
		return site == null ? "" : site;
	}
	
	public String getSite() {
		if (config.isEnableSitePath()) {
			String site = ContextHolder.get().getSiteName();
			return site == null ? "" : '/' + site;
		} else {
			return "";
		}
	}
	
	public String getBaseSite() {
		StringBuilder baseSite = new StringBuilder();
		String base = getBase();
		if (base.length() > 0) {
			baseSite.append(base);
		}
		String site = getSite();
		if (site.length() > 0) {
			baseSite.append('/').append(site);
		}
		return baseSite.toString();
	}
	
	//////////////////////////// collection function //////////////////////////
	public double sum(Collection<?> coll) {
		if (coll == null) {
			return 0;
		}
		
		double sum = 0;
		for (Object item : coll) {
			double value = 0;
			try {
				value = Double.parseDouble(item.toString());
			} catch (Exception ex) {
				logger.debug(ex);
			}
			sum += value;
		}
		return sum;
	}
	
	public List<?> select(Collection<?> coll, String propertyName, Object defaultValue) {
		List<Object> values = new ArrayList<Object>(coll.size());
		for (Object item : coll) {
			if (propertyName != null) {
				item = val(item, propertyName);
			}
			if (item == null) {
				item = defaultValue;
			}
			values.add(item);
		}
		return values;
	}
	
	// index the property ignoring the type
	public Object indexof(Collection<?> coll, String propertyName, Object value) {
		Object found = null;
		for (Object item : coll) {
			Object val = val(item, propertyName);
			String str = s(val, propertyName);
			if (value.equals(str)) {
				found = item;
				break;
			}
		}
		return found;
	}
	
	/**
	 * check if 'item' contains 'value', ignore the value type 
	 * 
	 * @param item item can be string(split by ','), array, collection
	 * @param value simple object or collection, any value matched is OK if 'value' is collection
	 * @return
	 */
	public boolean contains(Object item, Object value) {
		if (item == null) {
			return false;
		}
		if (value == null) {
			return true;
		}
		if (item instanceof String && !(value instanceof Collection || value.getClass().isArray())) {	// faster check
			return (((String)item) + ',').indexOf(value.toString() + ',') >= 0;
		}
		
		Collection coll;
		if (item instanceof Collection) {
			coll = (Collection)item;
		} else if (item.getClass().isArray()){
			Object[] arr = (Object[])item;
			coll = Arrays.asList(arr);
		}
		else {
			coll = StringUtil.splitToList(item.toString(), ',');
		}
		
		boolean contain = false;
		if (value instanceof Collection) {
			Collection theValue = (Collection) value;
			for (Object val : theValue) {
				contain = coll.contains(val);
				if (contain) {
					break;
				}
			}
		} else {
			contain = coll.contains(value);
		}
		return contain;
	}
	
	public String join(Collection<?> coll, String separator) {
		return StringUtils.join(coll, separator);
	}
	
	public List<String> addPrefix(Collection<String> coll, String prefix) {
		List<String> newList = new ArrayList<String>(coll.size());
		for (String str : coll) {
			newList.add(prefix + str);
		}
		return newList;
	}
	
	public List insert(List coll, int index, Object val) {
		List newColl = new ArrayList(coll.size() + 1);
		newColl.addAll(coll);
		newColl.add(index, val);
		return newColl;
	}
	
	public String[] toArray(Collection<String> coll) {
		return coll.toArray(new String[coll.size()]);
	}
	
	public boolean isCollection(Object obj) {
		return obj instanceof Collection;
	}
	
	public List<Object> toList(Object obj) {
		if (obj instanceof List) {
			return (List<Object>)obj;
		} else {
			List<Object> list = new ArrayList<Object>(1);
			list.add(obj);
			return list;
		}
	}
	
	public List<KeyValue> toKeyValue(Map<?, ?> objs) {
		List<KeyValue> list = new ArrayList<KeyValue>(objs.size());
		for (Entry<?, ?> entry : objs.entrySet()) {
			String key = entry.getKey() == null ? null : entry.getKey().toString();
			String value = entry.getValue() == null ? null : entry.getValue().toString();
			list.add(new KeyValue(key, value));
		}
		return list;
	}
	
	public List<KeyValue> toKeyValue(Collection<Object> objs, String additionalValue) {
		int size = objs == null ? 0 : objs.size();
		if (additionalValue != null && additionalValue.length() > 0) {
			size++;
		}
		List<KeyValue> list = new ArrayList<KeyValue>(size);
		if (size > 0) {
			if (additionalValue != null && additionalValue.length() > 0) {
				list.add(new KeyValue(additionalValue, additionalValue));
			}
			
			if (objs != null) {
				for (Object obj : objs) {
					String s = obj.toString();
					list.add(new KeyValue(s, s));
				}
			}
		}
		return list;
	}
	////////////////////////////collection function - end //////////////////////////
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Map joinMap(Map m1, Map m2) {
		if (m1 == null || m1.isEmpty()) {
			return m2;
		}
		if (m2 == null || m2.isEmpty()) {
			return m1;
		}
		Map ret = new HashMap();
		ret.putAll(m1);
		ret.putAll(m2);
		return ret;
	}
	
	// convert value to string, don't convert it
	public String toStr(Object val) {
		return val == null ? "" : val.toString();
	}
	
	public String s(Object val) {
		return s(val, null, true);
	}
	/**
	 * Convert val to string
	 * 
	 * @param val
	 * @return
	 */
	public String s(Object val, String property) {
		return s(val, property, true);
	}
	// toString
	public String s(Object val, String property, boolean quote) {
		String sVal;
		if (val != null) {
			if (val instanceof String) {
				sVal = (String)val;
			} else {
				sVal = ognlExUtil.convertValue(val, String.class, property);
				if (sVal == null) {
					sVal = "";
				}
			}
			
			if (quote && sVal.length() > 0) {
				sVal = FormatUtils.quote(sVal);
			}
		} else {
			sVal = "";
		}
		return sVal;
	}
	
	public String format(Object date, String format) {
		if (date == null) {
			return "";
		} else if (date instanceof String) {
			String str = (String)date;
			if (str.length() == 0) {
				return "";
			} else {
				Date converted = ognlExUtil.convertValue(str, Date.class);
				if (converted == null) {
					return str;
				} else {
					date = converted;
				}
			}
		}
		
		try {
			String sDate;
			if (date instanceof Number) {
				sDate = new DecimalFormat(format).format(date);
			} else {				
				sDate = new SimpleDateFormat(format).format(date);
			}
			return sDate;
		} catch (Exception ex) {
			logger.error("Wrong data format:" + format);
			return date.toString();
		}
	}
	
	public String max(String val, int max, String ellipsis) {
		if (val.length() > max) {
			return val.substring(0, max) + ellipsis;
		} else {
			return val;
		}
	}
	
	public String quote(String val) {
		return FormatUtils.quote(val);
	}
	
	public String escape(Object val, String property) {
		if (val instanceof String) {
			return FormatUtils.escape((String)val);
		} else {
			String str = ognlExUtil.convertValue(val, String.class, property);
			return str == null ? "" : str;
		}
	}
	
	public boolean equals(Object o1, Object o2) {
		if (o1 == null && o2 == null) {
			return true;
		}
		if (o1 == null || o2 == null) {
			return false;
		}
		
		if (o1.getClass().equals(o2.getClass())) {
			return o1.equals(o2);
		} else {
			if (!(o1 instanceof String)) {
				Class targetClass = o1.getClass();
				o2 = ognlExUtil.convertValue(o2, targetClass, null);
			} else {
				Class targetClass = o2.getClass();
				o1 = ognlExUtil.convertValue(o1, targetClass, null);
			}
			return o1 == null && o2 == null || o1 != null && o1.equals(o2);
		}
	}
	
	public int toInt(Object val, String property) {
		return ognlExUtil.convertValue(val, Integer.class, property);
	}
	
	public String toHtml(String v) {
		if (v != null) {
			return StringUtils.replace(v, "\n", "<br>");
		}
		return v;
	}
	///////////////////////////////////////////////////////////////////////////
	
	/////////////////////////////// service function //////////////////////////
	public String getModelName() {
		String className = ActionContext.getContext().getValueStack().findString("#actionName");
		String simpleName = beanIntrospector.getSimpleName(className);
		String modelName = beanIntrospector.getModelName(simpleName);
		return modelName;
	}
	
	/**
	 * create the model instance according to current path request
	 * 
	 * @return
	 */
	public Enum enums(String className, String property) {
		try {
			Class clazz = ClassUtils.getClass(className);
			return Enum.valueOf(clazz, property);
		} catch (ClassNotFoundException ex) {
			logger.error("Failed to get enum: " + className + '.' + property, ex);
			return null;
		}
	}
	
	public Object newModel() {
		return newModel(getModelName());
	}
	
	public Object newModel(String className) {
		return objectFactory.buildModel(className, new HashMap());
	}
	
	@SuppressWarnings("rawtypes")
	public Object newModel(String className, Map<String, Object> values) {
		Object model = objectFactory.buildModel(className, new HashMap());
		ModelUtil.copyProperties(values, model);
		return model;
	}
	
	public List<KeyValue> enumList(String enumClass, String fieldName) {
		List<KeyValue> enumList = new ArrayList<KeyValue>();
		
		try {
			Class clazz = null;
			if (enumClass == null || enumClass.isEmpty()) {
				ActionContext context = ActionContext.getContext();
				if (context != null) {
					String actionName = (String) context.getValueStack().
							getContext().get(FrameConstant.STACK_ACTIONNAME);
					
					String simpleModelName = beanIntrospector.getSimpleName(actionName);
					String modelName = beanIntrospector.getModelName(simpleModelName);
					clazz = ClassUtils.getClass(modelName);
				}
			} else {			
				clazz = ClassUtils.getClass(enumClass);
			}
			
			if (clazz != null) {
				Class parentClass = clazz;
				if (fieldName != null && !fieldName.isEmpty()) {
					if (fieldName.startsWith("model.")) {
						fieldName = fieldName.substring(6);
					} else if (fieldName.startsWith("execute.")) {
						fieldName = fieldName.substring(8);
					}
					ModelPropertyInfo modelPropertyInfo = beanIntrospector.getModelPropertyInfo(clazz.getName(), fieldName);
					if (modelPropertyInfo != null) {
						clazz = modelPropertyInfo.propertyInfo.getTypeClass();				
					}
				}
				
				if (clazz.isEnum()) {
					Method enumConstantDirectory = clazz.getDeclaredMethod("values");
					Object[] result = (Object[])enumConstantDirectory.invoke(null);
					for (Object item : result) {
						Enum enumItem = (Enum)item;
						enumList.add(new KeyValue("" + enumItem.ordinal(), enumItem.name()));
					}
				} else if (fieldName != null && !fieldName.isEmpty()){
					// way 1: use the upper case as the prefix
					String fieldPrefix = fieldName.toUpperCase() + '_';
					buildEnumList(parentClass, fieldPrefix, fieldName, enumList);
					if (enumList.isEmpty()) {
						// way 2:  
						StringBuilder sbFieldPrefix = new StringBuilder(fieldName.length() + 5);
						boolean previosCharIsSmallCase = false;
						for (int i = 0, len = fieldName.length(); i < len; i++) {
							char c = fieldName.charAt(i);
							if (Character.isUpperCase(c)) {
								if (previosCharIsSmallCase) {
									sbFieldPrefix.append('_');
								}
								previosCharIsSmallCase = false;
							} else {
								previosCharIsSmallCase = true;
							}
							sbFieldPrefix.append(Character.toUpperCase(c));
						}
						sbFieldPrefix.append('_');
						buildEnumList(parentClass, sbFieldPrefix.toString(), fieldName, enumList);
					}
				}
			}
		} catch (Exception ex) {
			logger.error("Failed to load constants", ex);
		}
		return enumList;
	}

	private void buildEnumList(Class parentClass, String fieldPrefix, String fieldName, List<KeyValue> enumList)
			throws IllegalAccessException {
		Field[] fields = parentClass.getFields();
		int len = fieldPrefix.length();
		
		for (Field field : fields) {
			if (field.getName().startsWith(fieldPrefix)) {
				Object val = field.get(null);
				String sVal;
				if (val instanceof String) {
					sVal = (String)val;
				} else {
					sVal = ognlExUtil.convertValue(val, String.class, fieldName);
				}
				enumList.add(new KeyValue(sVal, field.getName().substring(len)));
			}
		}
	}
	
	public Class getClass(String name) {
		return ClassUtil.getClass(name);
	}
	
	public Object bean(String name) {
		try {	
			return objectFactory.getBean(name, false);
		} catch (Exception ex) {
			logger.error("Fail to create bean: " + name + " (" + name + "), please check if the class exists", ex);
		}
		return null;
	}
	
	public Object load(Object model) {
		return getGenericService().load(model);
	}
	
	public Object load(String name, Integer id) {
		return getGenericService().load(ClassUtil.getClass(name), id);
	}

	public Object loadModel(String id) {
		Object model = newModel();
		ModelUtil.setSimpleProperty(model, "id", id);
		return getGenericService().load(model);
	}
	
	public Object service(String name) {
		Object service = commonServiceMap.get(name);
		if (service == null) {
			String serviceName = beanIntrospector.getServiceName(name);
			service = bean(serviceName);
		}
		return service;
	}
	
	/**
	 * Call the service method and support null parameter (like Javascript method invoker)
	 * 
	 * @param simpleName
	 * @param method
	 * @param parameters
	 * @return
	 */
	public Object service(String simpleName, String methodName, List<Object> parameters) {
		String serviceName;
		if (beanIntrospector.isService(simpleName) >= 0) {
			serviceName = simpleName;
		} else {
			serviceName = beanIntrospector.getServiceName(simpleName);
		}
		Object result = getInvocationUtil().invokeAndConvertParam(serviceName, methodName, parameters, true);
		return result;
	}
	
	
	public List<Object> vals(Object bean, String name) {
		Object val = ModelUtil.getProperty(bean, name);
		if (val instanceof List) {
			if (val == null) {
				return Collections.EMPTY_LIST;
			} else {
				return (List<Object>) val;
			}
		} else {
			List<Object> ret = new ArrayList<Object>(1);
			ret.add(val);
			return ret;
		}
	}


	public void setVal(Object model, String propertyName, Object value) {
		ModelUtil.setProperty(model, propertyName, value);
	}
	
	/**
	 * Get value, DONOT check the permission & DONOT return null value
	 * 
	 * @param bean
	 * @param property
	 * @param defaultVal
	 * @return
	 */
	public Object safeVal(Object bean, String property, Object defaultVal) {
		Object val;
		
		String realProperty;
		if (property.length() > 0 && property.charAt(0) == '_') {
			realProperty = null;
			if (property.startsWith("_gen_")) {				// see TagAdaptorUtil.getFullName
				if (property.charAt(6) != '_') {			// "_gen__"
					int end = property.indexOf('_', 5);		// 5 is "_gen_".length()
					if (end > 5) {
						realProperty = property.substring(5, end);
					}
				}
			}
		} else {
			realProperty = property;
		}
		if (realProperty != null && realProperty.length() > 0) {
			val = val(bean, realProperty, null, null, null);
		} else {
			val = null;
		}
		return val == null ? defaultVal : val;
	}
	
	/**
	 * Get value, don't check the permission
	 * 
	 * @param bean
	 * @param property
	 * @return
	 * @see val(Object bean, String property, String beanName)
	 */
	public Object val(Object bean, String property) {
		return val(bean, property, null, null, null);
	}
	
	/**
	 * Get value only when the property is valid property
	 * 
	 * @param bean
	 * @param property
	 * @param permissionCheckName
	 * @return
	 */
	public Object val4ValidName(Object bean, String property, String permissionCheckName) {	
		// see Element.isValidName
		boolean isValidProperty = isValidName(property);
		if (!isValidProperty) {
			return bean;
		}
		return val(bean, property, permissionCheckName);
	}
	
	/**
	 * Get value with permission check
	 * 
	 * @param bean
	 * @param property
	 * @param permissionCheckName the variable name of 'bean', used by the page permission check
	 * @return
	 */
	public Object val(Object bean, String property, String permissionCheckName) {
		HttpServletRequest request = ServletActionContext.getRequest();
		Context secContext = (Context) request.getAttribute("_SecContext_");
		String path = Routing.getUri(request);
		if (secContext == null) {
			ValueStack stack = ServletActionContext.getValueStack(request);
			secContext = new Context(stack, this);
			UserInfo userInfo = safeManager.getLoginUser(request);
			secContext.setUserInfo(userInfo);
			request.setAttribute("_SecContext_", secContext);
		}
		if (secContext.getUserInfo() == null) {
			return null;
		} else {
			return val(bean, property, secContext, path, permissionCheckName);
		}
	}
	
	public boolean isValidName(String name) {
		return name != null && name.length() > 0 && name.charAt(0) != '_';
	}
	
	public Object val(Object bean, String property, Context secContext, String path, String permissionCheckName) {
		int len = property.length();
		if (property == null || len == 0) {
			return bean;
		}
		if (property.charAt(len - 1) == '.') {				// format the property name, remove the invalid char
			property = property.substring(0, len - 1);
		}
		
		String funcName = null;
		boolean isFuncExpression = property.charAt(0) == ':';
		String[] args = null;
		if (isFuncExpression) {
			int index = property.indexOf('.', 1);
			if (index > 0) {
				funcName = property.substring(1, index);
				int end = property.indexOf('(', index + 1);
				if (end == -1) {
					property = property.substring(index + 1);
				} else {
					args = StringUtils.split(property.substring(end + 1, property.length() - 1), ',');
					property = property.substring(index + 1, end);
				}
				
				if (permissionCheckName != null) {
					int pos = permissionCheckName.indexOf(':');
					if (pos >= 0) {
						permissionCheckName = permissionCheckName.substring(0, pos) + property;
					}
				}
			}
		}
		
		Object val;
		try {
			if (bean == null || bean instanceof String) {	// NOTICE: don't get the child property of the string object
				val = null;
			} else if (property == null) {
				val = bean;
			} else {
				if (secContext == null){
					//val = MVEL.eval(property, bean);
					val = ModelUtil.getProperty(bean, property);
				} else {
					// TODO: is there better performance: reduce the count of get map?
					Object tmpVal;
					String subProperty;
					int sep = property.lastIndexOf('.');
					if (sep == -1) {
						tmpVal = bean;
						subProperty = property;
					} else {
						//tmpVal = MVEL.eval(property.substring(0, sep), bean);
						tmpVal = ModelUtil.getProperty(bean, property.substring(0, sep));
						subProperty = property.substring(sep + 1);
					}
					if (tmpVal == null) {
						val = null;
					} else {
						// 1. check page permission
						secContext.setModel(bean);
						String userId = secContext.getUserInfo().getLoginId();
						String ret = safeManager.checkReadField(userId, path, secContext, true, permissionCheckName, property);
						if (ret != null) {
							val = ret;
						} else {
							// 2. check model permission
							tmpVal = HibernateUtil.unproxy(tmpVal);
							String modelName = ClassUtil.getClassName(tmpVal);
							secContext.setModel(tmpVal);
							ret = safeManager.checkReadField(userId, modelName, secContext, true, subProperty, subProperty);
							if (ret != null) {
								val = ret;
							} else {
								val = ModelUtil.getProperty(tmpVal, subProperty);
								//val = MVEL.eval(subProperty, tmpVal);
							}
						}
					}
				}
			}
		} catch (Exception ex) {
			logger.error("Failed to get property:" + property, ex);
			val = null;
		}
		
		if (funcName != null) {
			val = exFuncs.get(funcName).execute(val, bean, property, args);
		}
		
		return val;
	}
	
	public boolean hasVal(Object val) {
		boolean hasVal = true;
		if (val instanceof String) {
			String sVal = (String) val;
			if (sVal.length() == 0) {
				hasVal = false;
			}
		}
		return hasVal;
	}
	///////////////////////////////////////////////////////////////////////////
	
	////////////////////////////// page function //////////////////////////////
	public boolean isInput(String method) {
		return method != null && method.startsWith(FrameConstant.ACTION_PREPARE);
	}

	/**
	 * Build link
	 * 
	 * @param action such as 'a/b/c'
	 * 
	 * @return
	 */
	public String link(String action) {
		return link(action, null);
	}
	
	public String link(String action, String parameters) {
		return link(action, parameters, true, true);
	}
	
	public String linkNoBase(String action, boolean withSite) {
		return link(action, null, withSite, false);
	}
	
	public String linkNoBase(String action, String parameters) {
		return link(action, parameters, false, false);
	}
	
	public List<String> linkNoBases(List<String> actions, boolean withSite) {
		if (actions == null) {
			return new ArrayList<String>(0);
		}
		
		List<String> newLinks = new ArrayList<String>(actions.size());
		for (String action : actions) {
			newLinks.add(linkNoBase(action, withSite));
		}
		return newLinks;
	}
	
	private String link(String action, String parameters, boolean withSite, boolean withBase) {
		if (action == null || action.length() == 0) {
			return "";
		} else if ("#".equals(action)) {
			return "#";
		} else if (action.startsWith("http://") || action.startsWith("https://")) {
			return action;
		}
		
		StringBuilder sb = new StringBuilder();
		
		// base
		if (withBase) {
			sb.append(getBase());
		}
		
		// free path prefix
		boolean isFreePath;
		String freePath = config.getSecurityFreePath();
		if (action.startsWith(freePath)) {
			sb.append(freePath);
			isFreePath =  true;
		} else {
			if (sb.length() == 0 || sb.charAt(sb.length() - 1) != '/') {
				sb.append('/');
			}
			isFreePath =  false;
		}
		
		// append site information
		if (withSite && config.isEnableSitePath()) {
			String site = ContextHolder.get().getSiteName();
			if (site != null) {
				sb.append(site).append('/');
			}
		}
		
		// action
		if (action.length() > 0) {
			if (isFreePath) {
				sb.append(action.substring(freePath.length()));
			} else {
				if (action.charAt(0) == '/') {
					sb.append(action.substring(1));
				} else {
					sb.append(action);
				}
			}
			
			// extension
			if (action.indexOf(actionExtensionSuffix) == -1) { 
				sb.append(actionExtensionSuffix);
			}
		}
		
		// parameter
		if (parameters != null && parameters.length() > 0) {
			if (sb.charAt(sb.length() -1) != '?') {
				sb.append('?');
			}
			sb.append(parameters);
		}
		return sb.toString();
	}
	
	public String linkModel(String modelName, String method) {
		if (modelName.indexOf('.') == -1) {		// invalid model name
			return "";
		}
		
		String simpleName = beanIntrospector.getSimpleName(modelName);
		String action = simpleName.replace('.', '/');
		if (method == null) {
			return link(action, null);
		} else {
			return link(action + '.' + method);
		}
	}
	
	public String linkModelNoBase(String modelName, String method) {
		return linkModelNoBase(modelName, method, false);
	}
	
	public String linkModelNoBase(String modelName, String method, boolean withSite) {
		String simpleName = beanIntrospector.getSimpleName(modelName);
		String action = simpleName.replace('.', '/');
		if (method == null) {
			return link(action, null, withSite, false);
		} else {
			return link(action + '.' + method, null, withSite, false);
		}
	}
	
	public String replaceAction(String action, String newMethod) {
		if (newMethod == null || newMethod.length() == 0) {
			return action.substring(0, action.lastIndexOf('.'));
		} else {
			if (action == null) {
				return "";
			} else {
				return action.substring(0, action.lastIndexOf('.') + 1) + newMethod;
			}
		}
	}
	
	public String getSubmitAction(String action) {
		if (action == null || action.length() == 0) {
			return "";
		}
		
		int index = action.lastIndexOf('.');
		if (index > 0 && action.charAt(index + 1) == '_') {
			String newAction = action.substring(0, action.lastIndexOf('.') + 1) + action.substring(index + 2);	// skip '_'
			return newAction;
		} else {
			return action;
		}
	}
	
	public String getSimpleName(String modelName) {
		if (modelName == null) {
			modelName = ClassUtil.getRuntimeType(GenericType.MODEL);
		}
		return beanIntrospector.getSimpleName(modelName);
	}
	
	public PageElements getTextModelPs(PageElements ps) {
		return new PageElementsTextModel(ps);
	}
	
	/**
	 * get the request page name
	 */
	public String page(String methodName) {
		return page("generic", methodName);
	}
	
	public String page(String simpleClassName, String methodName) {
		StringBuilder sb = new StringBuilder();
		String extension;
		if ("generic".equals(simpleClassName)) {
			sb.append(config.getCommonViewPkg());
			extension = "ftl";
		} else {
			sb.append(config.getViewDir());
			extension = actionExtension;
		}
		
		sb.append(simpleClassName.replace('.', '/'));
		if (methodName != null && methodName.length() > 0) {
			sb.append('.').append(methodName);
		}
		
		sb.append(".").append(extension);
		return sb.toString();
	}
	
	public String cssLink(String name) {
		return getBase() + css(name);
	}
	
	public String css(String name) {
		return config.getCssDir() + name + ".css";
	}

	@SuppressWarnings("unchecked")
	public Set<java.util.Map.Entry<String, Object>> entrySet() {
		return BlankObject.Set;
	}
	
	/**
	 * Avoid freemarker output the return value
	 * 
	 * @param value
	 */
	public void v(Object value) {
	}
	
	// page safe
	public String getId() {
		return '_' + String.valueOf(id++);
	}
	///////////////////////////////////////////////////////////////////////////
	
	public boolean hasPage(String urlWithoutSite) {
		Invoker invoker = routing.path2Invoker(urlWithoutSite, false);
		String simpleModelName = beanIntrospector.getSimpleName(invoker.actionName);
		return getPageConverter().hasPage(simpleModelName, invoker.methodName);
	}
	
	public String getElementPage(String urlWithoutSite, String elementName) {
		String elementPage = null;
		if (elementName != null && elementName.length() > 0) {
			elementPage = config.getSitePath() + config.getViewDir4GenCode() + urlWithoutSite + '.' + elementName + ".ftl";
			String checkPage = config.getRealWebRootPath() + elementPage;
			if (!new File(checkPage).exists()) {
				elementPage = null;
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("Include page: " + elementPage);
				}
			}
		}
		// ensure the page generated
		hasPage(urlWithoutSite);
		return elementPage;
	}
	
	////////////////////////////// debug function /////////////////////////////
	public String debugTag(String info) {
		if (isDebug()) {
			return "<!--" + info + "-->";
		} else {
			return "";
		}
	}
	
	public void debug(String pageName, Object object) {
		logger.debug(pageName + ':' + object);
	}
	
	public long getTime() {
		return System.currentTimeMillis();
	}
	///////////////////////////////////////////////////////////////////////////
	
	public TagUtil getTag() {
		if (tagUtil == null) {
			tagUtil = objectFactory.getBean(TagUtil.class, false);
		}
		return tagUtil;
	}
}